如果物件本身有負責計算的方法,且該方法依照給予的參數,會有不同的計算結果,那可以將計算的部分封裝成獨立的物件,彼此可以互相切換,同時不影響原有的功能。
試想一個情境,物件本身負責計算的功能,像是計算總金額、計算最佳路線、計算事件發生的機率等等,如果給予不同的參數,則計算的結果會大相徑庭。一段時間過後,方法可能會增加許多,多少增加管理上的麻煩。
為了方便管理,可以將計算的部分封裝成獨立的物件,而且物件本身都採用相同的規格,對外有一致的方法。如此一來,物件需要計算時,只要根據參數就能選定相關的計算物件,而且,採用相同的方法,呼叫任意計算物件都可以用相同的方法,簡化物件之間溝通上的麻煩。
在書上,會稱呼計算部分為演算法(Algorithm)。
作法是:
Context)Strategy)Context 本身會儲存目前使用的 Strategy,有計算需求時則使用 Strategy 的統一方法。Context 儲存的 Strategy。以下範例以「簡易動物園門票收費機」為核心製作。
製作入園者物件:Person
public class Person {
    private String name;
    private boolean hasStudentID;
    public Person(String name, boolean hasStudentID) {
        this.name = name;
        this.hasStudentID = hasStudentID;
    }
    public String getName() {
        return name;
    }
    public boolean isHasStudentID() {
        return hasStudentID;
    }
}
製作計算物件的虛擬層親代:Strategy
public interface Strategy {
    int calculateFees(List<Person> people);
}
製作計算物件的子代:StandardStrategy、GroupDiscountStrategy(Strategy 物件)
public class StandardStrategy implements Strategy {
    @Override
    public int calculateFees(List<Person> people) {
        int totalFees = 0;
        for (Person person : people) {
            if (person.isHasStudentID()) {
                totalFees += 30;
            } else {
                totalFees += 60;
            }
        }
        return totalFees;
    }
}
public class GroupDiscountStrategy implements Strategy {
    @Override
    public int calculateFees(List<Person> people) {
        int totalFees = 0;
        for (Person person : people) {
            if (person.isHasStudentID()) {
                totalFees += 30;
            } else {
                totalFees += 60;
            }
        }
        return (int) (totalFees * 0.7);
    }
}
製作動物園門票售票機:ZoomTicketVendingMachine(Context 物件)
public class ZoomTicketVendingMachine {
    private Strategy strategy;
    private List<Person> people;
    public ZoomTicketVendingMachine() {
        people = new ArrayList<>();
    }
    public void setStrategy(int peopleCounts) {
        if (peopleCounts >= 30) {
            strategy = new GroupDiscountStrategy();
        } else {
            strategy = new StandardStrategy();
        }
    }
    public void addPerson(Person person) {
        people.add(person);
    }
    public void removePerson(Person person) {
        people.remove(person);
    }
    public int calculateFees() {
        setStrategy(people.size());
        return strategy.calculateFees(people);
    }
    public void clear() {
        people.clear();
    }
}
測試,模擬一家四口以及校外校學買動物園門票:TicketMachineStrategySample
public class TicketMachineStrategySample {
    public static void main(String[] args) throws Exception {
        ZoomTicketVendingMachine ticketMachine = new ZoomTicketVendingMachine();
        System.out.println("---一家四口,兩大兩小---");
        ticketMachine.addPerson(new Person(NameSelector.exec("m"), false));
        ticketMachine.addPerson(new Person(NameSelector.exec("f"), false));
        ticketMachine.addPerson(new Person(NameSelector.exec("m"), true));
        ticketMachine.addPerson(new Person(NameSelector.exec("f"), true));
        int familyFees = ticketMachine.calculateFees();
        System.out.println("家庭的總金額是: " + familyFees);
        ticketMachine.clear();
        System.out.println("---戶外教學,兩個導師以及三十八個學生---");
        ticketMachine.addPerson(new Person(NameSelector.exec("m"), false));
        ticketMachine.addPerson(new Person(NameSelector.exec("f"), false));
        for (int i = 0; i < 19; i++) {
            ticketMachine.addPerson(new Person(NameSelector.exec("f"), true));
        }
        for (int i = 0; i < 19; i++) {
            ticketMachine.addPerson(new Person(NameSelector.exec("m"), true));
        }
        int schoolTripFees = ticketMachine.calculateFees();
        System.out.println("校外教學的總金額是: " + schoolTripFees);
    }
}
Utils:NameSelector
public class NameSelector {
    private static Random random = new Random();
    public static String exec(String gender) throws IOException, ParseException, Exception {
        // 讀取 JSON Array,內容是字串陣列
        JSONParser parser = new JSONParser();
        Object obj = null;
        if (gender.equals("m")) {
            obj = parser.parse(new FileReader("./src/utils/boyNameList.json"));
        } else if (gender.equals("f")) {
            obj = parser.parse(new FileReader("./src/utils/girlNameList.json"));
        } else {
            throw new Exception("性別代號錯誤!\n輸入的性別參數是: " + gender);
        }
        JSONArray jsonArray = (JSONArray) obj;
        // 0 - 149
        int option = random.nextInt(149 + 0) + 0;
        return (String) jsonArray.get(option);
    }
}
製作入園者物件:Person
class Person {
  /**
   * @param {string} name
   * @param {boolean} hasStudentID
   */
  constructor(name, hasStudentID) {
    this.name = name;
    this.hasStudentID = hasStudentID;
  }
  getName() {
    return this.name;
  }
  isHasStudentID() {
    return this.hasStudentID;
  }
}
製作計算物件的虛擬層親代:Strategy
/**
 * @abstract
 */
class Strategy {
  /**
   * @abstract
   * @param {Person[]} people
   */
  calculateFees(people) { return 0; }
}
製作計算物件的子代:StandardStrategy、GroupDiscountStrategy(Strategy 物件)
class StandardStrategy extends Strategy {
  /**
   * @override
   * @param {Person[]} people
   */
  calculateFees(people) {
    let totalFees = 0;
    for (const person of people) {
      if (person.isHasStudentID()) {
        totalFees += 30;
      } else {
        totalFees += 60;
      }
    }
    return totalFees;
  }
}
class GroupDiscountStrategy extends Strategy {
  /**
  * @override
  * @param {Person[]} people
  */
  calculateFees(people) {
    let totalFees = 0;
    for (const person of people) {
      if (person.isHasStudentID()) {
        totalFees += 30;
      } else {
        totalFees += 60;
      }
    }
    return totalFees * 0.7;
  }
}
製作動物園門票售票機:ZoomTicketVendingMachine(Context 物件)
class ZoomTicketVendingMachine {
  constructor() {
    /** @type {Strategy} */
    this.strategy = null;
    /** @type {Person[]} */
    this.people = [];
  }
  /** @param {number} peopleCounts */
  setStrategy(peopleCounts) {
    if (peopleCounts >= 30) {
      this.strategy = new GroupDiscountStrategy();
    } else {
      this.strategy = new StandardStrategy();
    }
  }
  /** @param {Person} person */
  addPerson(person) {
    this.people.push(person);
  }
  /** @param {Person} person */
  removePerson(person) {
    this.people = this.people.filter(item => item !== person);
  }
  calculateFees() {
    this.setStrategy(this.people.length);
    return this.strategy.calculateFees(this.people);
  }
  clear() {
    this.people = [];
  }
}
測試,模擬一家四口以及校外校學買動物園門票:ticketMachineStrategySample
const ticketMachineStrategySample = () => {
  const ticketMachine = new ZoomTicketVendingMachine();
  console.log("---一家四口,兩大兩小---");
  ticketMachine.addPerson(new Person(nameSelector("m"), false));
  ticketMachine.addPerson(new Person(nameSelector("f"), false));
  ticketMachine.addPerson(new Person(nameSelector("m"), true));
  ticketMachine.addPerson(new Person(nameSelector("f"), true));
  const familyFees = ticketMachine.calculateFees();
  console.log("家庭的總金額是: " + familyFees);
  ticketMachine.clear();
  console.log("---戶外教學,兩個導師以及三十八個學生---");
  ticketMachine.addPerson(new Person(nameSelector("m"), false));
  ticketMachine.addPerson(new Person(nameSelector("f"), false));
  for (let i = 0; i < 19; i++) {
    ticketMachine.addPerson(new Person(nameSelector("f"), true));
  }
  for (let i = 0; i < 19; i++) {
    ticketMachine.addPerson(new Person(nameSelector("m"), true));
  }
  const schoolTripFees = ticketMachine.calculateFees();
  console.log("校外教學的總金額是: " + schoolTripFees);
}
ticketMachineStrategySample();
Utils:nameSelector
/** @param {string} gender */
const nameSelector = (gender) => {
  const option = Math.floor(Math.random() * (149 - 0 + 1)) + 0;
  // 讀取 JSON Array,內容是字串陣列
  if (gender === "m") {
    const boyNameList = require('../Sample-by-Java/src/utils/boyNameList.json');
    return boyNameList[option];
  } else if (gender === "f") {
    const girlNameList = require('../Sample-by-Java/src/utils/girlNameList.json');
    return girlNameList[option];
  } else {
    throw new Error(`性別參數錯誤,輸入的參數是: ${gender}`);
  }
}
Strategy 模式跟 Simple Factory Method 十分類似,皆擁有 if - else if - else 或 switch case 而有多個可能的選擇,兩者最大的不同在於前者專注在將 Business Logic 抽出;後者專注在如何「產出」需要的物件。當然,兩者的類別不同,理所當然在乎不同的點。
實作上要注意的,與 State 模式相似,什麼樣的情境下,需要將 if - else if - else 或 switch case 抽出、封裝成獨立物件?就我工作經驗來說,如果之後在開發上會建立許多負責計算的物件,那可以提早封裝,節省之後要套用 Strategy 模式的時間。反之,如果不會建立許多負責計算的物件,那不用套用 Strategy 模式,維持 if - else if - else 或 switch case 也很好。
明天將介紹 Behavioural patterns 的第十個模式:Template Method 模式。